package com.gitee.loyo;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.gitee.loyo.utils.HttpUtils;
import io.vertx.core.*;
import io.vertx.core.http.*;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonObject;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Base64;
import java.util.Enumeration;
import java.util.HashSet;

import static com.gitee.loyo.Const.WEBSOCKET_CLIENT_HEADER;

public class Application extends AbstractVerticle {
    private static Logger log = LoggerFactory.getLogger(Application.class);
    public static HttpClient retryHttpClient;

    public static void main(String[] args) throws URISyntaxException, IOException {
        File runningFile = new File(Application.class.getProtectionDomain().getCodeSource().getLocation().toURI());
        Global.RUNNING_DIR = runningFile;
        if(Global.RUNNING_DIR.isFile()){
            Global.RUNNING_DIR = Global.RUNNING_DIR.getParentFile();
        }
        log.info("running on " + Global.RUNNING_DIR.getPath());

        Global.CONF_DIR = new File(Global.RUNNING_DIR, "conf");
        Global.BRIDGE_FILE = new File(Global.RUNNING_DIR, "bridge.json");

        if(Global.BRIDGE_FILE.exists()){
            String content = new String(Files.readAllBytes(Global.BRIDGE_FILE.toPath()), StandardCharsets.UTF_8);
            BridgeConfig.INSTANCE = content.isEmpty() ? new BridgeConfig() : Json.decodeValue(content, BridgeConfig.class);
            if(BridgeConfig.INSTANCE.proxies != null){
                for (Endpoints.Proxy proxy : BridgeConfig.INSTANCE.proxies) {
                    Endpoints.endpoints.put(proxy.id, proxy);
                }
            }
        }else{
            Global.BRIDGE_FILE.createNewFile();
            BridgeConfig.INSTANCE = new BridgeConfig();
        }

        Global.MACHINE_IP_POOL = new HashSet<>();
        Global.MACHINE_IP_POOL.add(System.getProperty("accessible.ip", "localhost"));
        Enumeration<NetworkInterface> netEnum = NetworkInterface.getNetworkInterfaces();
        while (netEnum.hasMoreElements()){
            NetworkInterface networkInterface = netEnum.nextElement();
            for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) {
                Global.MACHINE_IP_POOL.add(address.getAddress().getHostAddress());
            }
        }
        for (String address : Global.MACHINE_IP_POOL) {
            log.info("accessible ip: {}", address);
        }

        VertxOptions vertxConfig = new VertxOptions();
        JsonObject globeConfig = new JsonObject();
        if(Global.CONF_DIR.exists() && Global.CONF_DIR.isDirectory()){
            StringBuilder sb = new StringBuilder();
            for (File yamlFile : Global.CONF_DIR.listFiles((dir, name) -> name.endsWith(".yaml"))) {
                if(yamlFile.isFile() && yamlFile.canRead()){
                    System.out.println("read config from " + yamlFile.getPath());
                    sb.append("# ").append(yamlFile.getName()).append("\n");
                    sb.append(new String(Files.readAllBytes(yamlFile.toPath()), StandardCharsets.UTF_8));
                }
            }
            ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
            Object yamlObj = yamlReader.readValue(sb.toString(), Object.class);
            globeConfig = new JsonObject(Json.encode(yamlObj));
            vertxConfig = new VertxOptions(globeConfig);
        }

        log.info("config：\n" + globeConfig.encodePrettily());

        Global.VELOCITY_ENGINE = new VelocityEngine();
        Global.VELOCITY_ENGINE.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
        Global.VELOCITY_ENGINE.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());

        Vertx vertx = Vertx.vertx(vertxConfig);
        vertx.deployVerticle(new Application(), new DeploymentOptions().setConfig(globeConfig), ar -> {
            if(ar.failed()){
                ar.cause().printStackTrace(System.err);
                System.exit(0);
                return;
            }else{
                System.out.println("API-Bridge Starting...");
            }
        });

    }

    @Override
    public void start(Promise<Void> promise) {
        HttpServerOptions httpServerOptions = new HttpServerOptions(config());
        vertx.createHttpServer(httpServerOptions)
            .connectionHandler(conn -> {
                Global.ALIVE_CONNECTION.add(conn);
                conn.exceptionHandler(t ->
                    log.error("connection error", t)
                ).closeHandler(event ->
                    Global.ALIVE_CONNECTION.remove(conn)
                );
            })
            .exceptionHandler(throwable -> log.error("http error", throwable))
            .webSocketHandler(webSocket -> {
                webSocket.pause();
                String host = webSocket.host();
                if(Global.MACHINE_IP_POOL.contains(HttpUtils.splitPort(host))){
                    handleClientWebSocket(webSocket);
                }else{
                    handleProxyWebSocket(webSocket);
                }
                webSocket.resume();
            })
            .requestHandler(request -> {
                String host = request.host();
                if(Global.MACHINE_IP_POOL.contains(HttpUtils.splitPort(host))){
                    handleConfigRequest(request);
                }else{
                    handleProxyRequest(request);
                }
            })
            .invalidRequestHandler(request ->
                request.response().setStatusCode(400).setStatusMessage("Invalid Request").end()
            )
            .listen(ar -> {
                if(ar.succeeded()){
                    log.info("API-Bridge listening to {}", ar.result().actualPort());
                    promise.complete();
                }else{
                    promise.fail(ar.cause());
                }
            });

        retryHttpClient = vertx.createHttpClient(new HttpClientOptions(config()).setDefaultHost("localhost").setDefaultPort(httpServerOptions.getPort()));
    }

    private void handleClientWebSocket(ServerWebSocket webSocket) {
        try{
            String clientId = webSocket.headers().get(WEBSOCKET_CLIENT_HEADER);
            if(clientId == null || clientId.isEmpty()){
                webSocket.reject(403);
                return;
            }
            clientId = new String(Base64.getDecoder().decode(clientId), StandardCharsets.UTF_8);
            Endpoints.registerClient(vertx, clientId, webSocket);
        }catch (Exception e){
            log.error("handle client web socket", e);
            webSocket.close((short) 400, "unhandled client id");
        }
    }

    private void handleProxyWebSocket(ServerWebSocket webSocket) {
    }

    private void handleConfigRequest(HttpServerRequest request) {
        if(Auth.basicAuthEnabled() && !Auth.checkBasicAuthToken(request)){
            HttpServerResponse response = request.response();
            Auth.needBasicAuth(response);
            return;
        }
        Pages.dispatch(request);
    }

    private void handleProxyRequest(HttpServerRequest request) {
        if(!Bridges.contains(request.host())) {
            request.response().setStatusCode(404).setStatusMessage("Not Found").end();
            return;
        }

        Bridges.Bridge bridge = Bridges.get(request.host());
        if(bridge.hostname == null || bridge.hostname.isEmpty()){
            request.response().setStatusCode(503).setStatusMessage("Host Not Configured").end();
            return;
        }

        if(!Endpoints.contains(bridge.hostname)){
            request.response().setStatusCode(503).setStatusMessage("Service Unavailable").end();
            return;
        }


        Endpoints.handle(vertx, bridge, request);
    }
}
